package com.sd365.gateway.authorization.service.impl;


import cn.hutool.crypto.digest.opt.TOPT;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sd365.common.util.StringUtil;
import com.sd365.common.util.TokenUtil;
import com.sd365.gateway.authorization.dao.mapper.ResourceMapper;
import com.sd365.gateway.authorization.entity.Resource;
import com.sd365.gateway.authorization.service.AuthorizationService;
import com.sd365.gateway.authorization.service.ResourcesService;
import com.sd365.gateway.authorization.service.RoleResourcesService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.swing.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.compile;

/**
 * @author Administrator
 * @version 1.0.0
 * @class WayBillServiceImpl
 * @classdesc  要求培训的学员 实现AuthorizationService 接口实现鉴权逻辑
 * 具体的鉴权的逻辑参考 实战指引手册的鉴权流程
 * @date 2020-10-2  18:04
 * @see
 * @since
 */
@Slf4j
@Service
public class AuthorizationServiceImpl implements AuthorizationService {
    /**
     * 匹配请求URL的正则表达式
     */
    private static final String AUTHOR_REQUEST_URL_EXPR="^https?:\\/\\/(?:[0-9a-zA-Z:\\.]*)([^\\?]*)";

    @Autowired
    private ResourcesService resourcesService;
    @Autowired
    private ResourceMapper resourceMapper;
    @Autowired
    private RoleResourcesService roleResourcesService;
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${hasOpenAuthorization}")
    private Boolean hasOpenAuthorization;

    /**
     * 续期时间三天 单位毫秒 1000 * 60 * 60 * 24 * 3 = 86400
     */
    private static final Long PERIOD = 259200000L;
    /**
     * 一小时
     */
    private static final Long ONE_DAY = 86400000L;

    private static final String USER_TOKEN_KEY = "user:token:";


    /**
     * 获取通用资源，避免网关鉴权访问数据库而影响请求过程
     * @author xiehl
     * @return  该资源是用户中心初始化存储的，用于鉴别么有纳入权限体系的资源
     */
    private List<Resource> getCommonResource() {
        List<Resource> commonResources =new ArrayList<>();
        try {
            //status=2当做缓存的key
            Object o = redisTemplate.opsForValue().get(String.valueOf(2));
            //获取全部的通用资源
            commonResources = JSONObject.parseArray(String.valueOf(o),Resource.class);


        } catch (Exception e) {
            log.error("通用资源缓存获取失败");
            throw new  RuntimeException("通用资源缓存获取失败",e);
        }
        if(CollectionUtils.isEmpty(commonResources)){
            commonResources = resourceMapper.commonResource();
        }
        return commonResources;
    }



    /**
     * 从缓存和数据库中获取resource
     * @param roleIdsList 用做key来从缓存中获取resource
     * @param roleIds 用于查询数据库中的resource
     * @return
     */
    private List<Resource> getResourceList(List<String> roleIdsList,List<Long> roleIds) {
        Assert.notEmpty(roleIdsList,"roleIdsList不能为空");
        Assert.notEmpty(roleIds,"roleIds不能为空");

       //查缓存或者数据库表鉴权
        List<Resource> resources = new ArrayList<>();
        try {
            List<Object> objectList = new ArrayList<>();
            for(String roleId : roleIdsList){
                objectList.add(redisTemplate.opsForValue().get(roleId));
            }
            for(Object o: objectList){
                //将缓存中的value转化成List对象
                List<Resource> resourceList = JSONObject.parseArray(String.valueOf(o), Resource.class);
                for(Resource resource : resourceList){
                    resources.add(resource);
                }
            }
        } catch (Exception e) {
            resources=null;
            log.info("缓存中没有对应的角色资源");
            e.printStackTrace();
        }
        if(CollectionUtils.isEmpty(resources)){
            //当redis中没有对应roleId的缓存或者redis异常时,鉴权服务访问数据库的资源表
            List<Long> resourceIds = roleResourcesService.searchResourceIdsByroleIds(roleIds);
            if (resourceIds == null || resourceIds.size() == 0) {
                return null;
            }
            resources = resourcesService.searchResource(resourceIds);
        }
        return resources;
    }

    /**
     *  从token中解析出角色id的list
     * @param token 从header中解析的 accessToken
     * @return 角色id数组
     */
    private List<String> getRoleIdsList(String token,List<Long> roleIds) {
        String tokenCode = new String(Base64.getDecoder().decode(token.getBytes()));
        JSONObject tokenJson = JSONObject.parseObject(tokenCode);
        JSONArray roleIdsJson = (JSONArray) tokenJson.get((Object) "roleIds");
        List<String> roleIdsList = JSONArray.toJavaObject(roleIdsJson, List.class);
        if(!CollectionUtils.isEmpty(roleIdsList)){
            for (String roleIdStr : roleIdsList) {
                roleIds.add(Long.parseLong(roleIdStr));
            }
        }else {
            throw new RuntimeException("roleIdsList为空异常");
        }
        return roleIdsList;
    }

    /**
     *  根据正则表达式匹配URL
     * @param url  URL
     * @param reg  正则表达式
     * @return  匹配后的URL 解析了请求的字符串
     */
    private String getMatchURL(String url,String reg){
       Assert.hasText(url,"url NOT NULL");
        Assert.hasText(reg,"url NOT NULL");
        String newUrl="";
        final Pattern compile = compile(reg);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            newUrl = matcher.group(1);
        }
        return newUrl;
    }

    private Boolean refreshTokenRedis(String userId){
        Assert.hasText(userId,"userId为空, refreshTokenRedis()参数异常");
        // 从redis中获取token
        String tokenKey = USER_TOKEN_KEY + userId;
        String redisToken = stringRedisTemplate.opsForValue().get(tokenKey);
        // 如果redis中没有读取到token 说明token过期了，需要重新登陆
        if (StringUtil.isEmpty(redisToken)){
            return false;
        }
        String header = null;
        String playLoad = null;
        String[] headerAndPlayLoad = redisToken.split("\\.");
        if (headerAndPlayLoad.length > 1) {
            header = headerAndPlayLoad[0];
            playLoad = headerAndPlayLoad[1];
        }
        // 对playLoad进行解密，获取json对象
        JSONObject tokenJson = getTokenJson(playLoad);
        Assert.notNull(tokenJson,"tokenJson为空");
        Date expiresAt = getTokenExpiresAt(tokenJson);
        Assert.notNull(expiresAt,"expiresAt过期时间为空");
        if (expiresAt.before(new Date())){
            // 判断key是否存在
            if (stringRedisTemplate.hasKey(tokenKey)){
                // 更新token中过期时间
                JSONObject updateTokenExpiresAt = updateTokenExpiresAt(tokenJson);
                // 更新token
                String updateToken = updateToken(header, updateTokenExpiresAt);
                // 重置redis中过期时间
                stringRedisTemplate.opsForValue().set(tokenKey,updateToken,PERIOD + ONE_DAY,TimeUnit.MILLISECONDS);
                return true;
            }
            return false;
        }
        return true;
    }

    /**
     * 获取过期时间
     * @param tokenJson
     * @return
     */
    private Date getTokenExpiresAt(JSONObject tokenJson){
        Assert.notNull(tokenJson,"tokenJson不能为空，getTokenExpiresAt()参数异常");
        Long expiresTime = (Long) tokenJson.get("expiresAt");
        Assert.notNull(expiresTime,"getTokenExpiresAt  expiresTime 获取时间失败");
        return new Date(expiresTime);
    }

    /**
     * 更新过期时间
     * @param tokenJson
     * @return
     */
    private JSONObject updateTokenExpiresAt(JSONObject tokenJson){
        Assert.notNull(tokenJson,"tokenJson为空 updateTokenExpiresAt参数错误");
        tokenJson.put("expiresAt",new Date(System.currentTimeMillis() + PERIOD));
        return tokenJson;
    }

    /**
     * 更新token
     * @param header
     * @param playLoad
     * @return
     */
    private String updateToken(String header,JSONObject playLoad){
        Assert.hasText(header,"header 为空，updateToken参数异常");
        Assert.notNull(playLoad,"playLoad 为空，updateToken参数异常");
        String encoderToken = TokenUtil.encoderToken(playLoad.toJSONString());
        return header + "." + encoderToken;
    }

    /**
     * 截取token的playLoad部分
     * @param token
     * @return token
     */
    private String parseToken(String token){
        Assert.hasText(token,"token为空 parseToken 参数异常");
        String parseToken = null;
        String[] jwtTokeItem = token.split("\\.");
        if (jwtTokeItem.length > 1) {
            parseToken = jwtTokeItem[1];
        }else {
            throw new RuntimeException("roleAuthorization 解析的token的第2个元素不存在");
        }
        parseToken = parseToken.substring(0, parseToken.length() - 1);
        return parseToken;
    }

    /**
     * 解析token获取JSON对象
     * @param token
     * @return
     */
    private JSONObject getTokenJson(String token){
        Assert.hasText(token,"token为空 getTokenJson参数异常");
        String tokenCode = new String(Base64.getDecoder().decode(token.getBytes()));

        Assert.hasText(tokenCode,"getTokenJson tokenCode不能为空");
        return JSON.parseObject(tokenCode);
    }

    /**
     * 获取UserId
     * @param token
     * @return
     */
    private Long getUserId(String token){
        Assert.hasText(token,"token为空 getUserId() 参数异常");

        JSONObject tokenJson = getTokenJson(token);
        Assert.notNull(tokenJson,"tokenJson 不能为空");

        Long userId = Long.valueOf(String.valueOf(tokenJson.get("userId")));
        Assert.notNull(userId,"userId 不能为空");
        return userId;
    }


    /**
     * 鉴权服务
     * 权限管理 根据用户对应权限动态获取左侧菜单栏
     * @param token 登录的用户session携带的token
     * @param url 请求的地址
     * @return
     */
    @Override
    public Boolean roleAuthorization(String token, String url) {
        /*
        对token 和 url 判断是否为空  为空则报错
         */
        Assert.hasText(token,"token为空 roleAuthorization 参数异常");
        Assert.hasText(url,"url为空 roleAuthorization 参数异常");
        List<Long> roleIds = new LinkedList<>();
        //解析token
        String parseToken = parseToken(token);
        Long userId = getUserId(parseToken);
        Boolean refreshTokenRedis = refreshTokenRedis(userId.toString());
        if (!refreshTokenRedis){
            return false;
        }

        if (!hasOpenAuthorization){
            return true;
        }
        // @TODO  etBytes的字符集要 utf-8 因为当前系统并未发生错误 所以日后以后 abel.zhan
        // 获取roleIds
        List<String> roleIdsList = getRoleIdsList(parseToken,roleIds);
        //提取url
        String authorURL = getMatchURL(url,AUTHOR_REQUEST_URL_EXPR);
        //获取角色资源
        List<Resource> resources = getResourceList(roleIdsList,roleIds);
        if (resources == null) {return false;}
        for (Resource resource : resources) {
            if (!StringUtils.isEmpty(resource.getApi())) {
                final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                final Matcher matcherApi = pattern.matcher(authorURL);
                if (matcherApi.find()) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public Boolean commonResource(String url) throws InterruptedException {
        final Pattern compile = compile(AUTHOR_REQUEST_URL_EXPR);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            url = matcher.group(1);
        }
        List<Resource> commonResources = getCommonResource();

        if (!CollectionUtils.isEmpty(commonResources)) {
            for (Resource resource : commonResources) {
                if (resource.getApi() != null) {
                    final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                    final Matcher matcherApi = pattern.matcher(url);
                    if (matcherApi.find()) {
                        return true;
                    }
                }
            }
        }
        return true;
    }
}
